﻿using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using Red.Controls.Diagrams.ViewModels;
using Red.Core;

namespace Red.Controls.Diagrams
{
    public enum DiagramLOD
    {
        Lod0,
        Lod1,
        Lod2,
    }

    /// <summary>
    /// The view-model for the graph context. It contains NetworkViewModel and also viewport properties.
    /// </summary>
    public class DiagramViewModel : ViewModel
    {
        #region Data

        /// <summary>
        /// This is the network that is displayed in the window.
        /// It is the main part of the view-model.
        /// </summary>
        private NetworkViewModel _network;

        ///
        /// The current scale at which the content is being viewed.
        /// 
        private double _contentScale = 1;

        ///
        /// The X coordinate of the offset of the viewport onto the content (in content coordinates).
        /// 
        private double _contentOffsetX;

        ///
        /// The Y coordinate of the offset of the viewport onto the content (in content coordinates).
        /// 
        private double _contentOffsetY;
        ///
        /// The width of the content (in content coordinates).
        /// 
        private double _contentWidth = 1;

        ///
        /// The heigth of the content (in content coordinates).
        /// 
        private double _contentHeight = 1;

        ///
        /// The width of the viewport onto the content (in content coordinates).
        /// The value for this is actually computed by the main window's ZoomAndPanControl and update in the
        /// view-model so that the value can be shared with the overview window.
        /// 
        private double _contentViewportWidth;

        ///
        /// The height of the viewport onto the content (in content coordinates).
        /// The value for this is actually computed by the main window's ZoomAndPanControl and update in the
        /// view-model so that the value can be shared with the overview window.
        /// 
        private double _contentViewportHeight;

        private DiagramLOD _lod = DiagramLOD.Lod0;

        private bool _isEditMode = true;

        private NodeOrientation _orientation;

        #endregion Data

        #region Properties

        public DiagramLOD LOD
        {
            get { return _lod; }
            set 
            { 
                _lod = value; 
                OnPropertyChanged();
            }
        }

        /// <summary>
        /// In EditMode we can draw new connections or drag nodes. 
        /// EditMode is disabled in NodeDeclaratorEditor.
        /// </summary>
        public bool IsEditMode
        {
            get { return _isEditMode; }
            set 
            { 
                _isEditMode = value;
                OnPropertyChanged();
            }
        }

        /// <summary>
        /// This is the network that is displayed in the window.
        /// It is the main part of the view-model.
        /// </summary>
        public NetworkViewModel Network
        {
            get
            {
                return _network;
            }
            set
            {
                _network = value;
                OnPropertyChanged();
            }
        }

        ///
        /// The current scale at which the content is being viewed.
        /// 
        public double ContentScale
        {
            get
            {
                return _contentScale;
            }
            set
            {
                _contentScale = value;
                if ( _contentScale < 0.2 )
                    LOD = DiagramLOD.Lod2;
                else if ( _contentScale < 0.4 )
                    LOD = DiagramLOD.Lod1;
                else
                    LOD = DiagramLOD.Lod0;
                OnPropertyChanged();
            }
        }

        ///
        /// The X coordinate of the offset of the viewport onto the content (in content coordinates).
        /// 
        public double ContentOffsetX
        {
            get
            {
                return _contentOffsetX;
            }
            set
            {
                _contentOffsetX = value;
                OnPropertyChanged();
            }
        }

        ///
        /// The Y coordinate of the offset of the viewport onto the content (in content coordinates).
        /// 
        public double ContentOffsetY
        {
            get
            {
                return _contentOffsetY;
            }
            set
            {
                _contentOffsetY = value;
                OnPropertyChanged();
            }
        }

        ///
        /// The width of the content (in content coordinates).
        /// 
        public double ContentWidth
        {
            get
            {
                return _contentWidth;
            }
            set
            {
                _contentWidth = value;
                OnPropertyChanged();
            }
        }

        ///
        /// The heigth of the content (in content coordinates).
        /// 
        public double ContentHeight
        {
            get
            {
                return _contentHeight;
            }
            set
            {
                _contentHeight = value;
                OnPropertyChanged();
            }
        }

        ///
        /// The width of the viewport onto the content (in content coordinates).
        /// The value for this is actually computed by the main window's ZoomAndPanControl and update in the
        /// view-model so that the value can be shared with the overview window.
        /// 
        public double ContentViewportWidth
        {
            get
            {
                return _contentViewportWidth;
            }
            set
            {
                _contentViewportWidth = value;
                OnPropertyChanged();
            }
        }

        ///
        /// The heigth of the viewport onto the content (in content coordinates).
        /// The value for this is actually computed by the main window's ZoomAndPanControl and update in the
        /// view-model so that the value can be shared with the overview window.
        /// 
        public double ContentViewportHeight
        {
            get
            {
                return _contentViewportHeight;
            }
            set
            {
                _contentViewportHeight = value;
                OnPropertyChanged();
            }
        }

        public NodeOrientation Orientation
        {
            get { return _orientation; }
            set
            {
                if ( _orientation != value )
                {
                    _orientation = value;
                    OnPropertyChanged();
                }
            }
        }

        #endregion Properties

        #region Event handlers

        /// <summary>
        /// Called when the user has started to drag out a connector, thus creating a new connection.
        /// </summary>
        public ConnectionViewModel ConnectionDragStarted( ConnectorViewModel draggedOutConnector, Point curDragPoint )
        {
            //
            // Disable dragging new connection from invalid connector.
            //
            if ( !draggedOutConnector.IsValid )
                return null;

            var connection = new ConnectionViewModel();

            if ( draggedOutConnector.Type == ConnectorType.Output )
            {
                //
                // The user is dragging out a source connector (an output) and will connect it to a destination connector (an input).
                //
                connection.SourceConnector = draggedOutConnector;
                connection.DestConnectorHotspot = curDragPoint;
            }
            else
            {
                //
                // The user is dragging out a destination connector (an input) and will connect it to a source connector (an output).
                //
                connection.DestConnector = draggedOutConnector;
                connection.SourceConnectorHotspot = curDragPoint;
            }

            //
            // Add the new connection to the view-model.
            //
            AddConnection( connection );

            return connection;
        }

        #endregion Event handlers

        #region Public methods

        /// <summary>
        /// Called as the user continues to drag the connection.
        /// </summary>
        public void ConnectionDragging( Point curDragPoint, ConnectionViewModel connection )
        {
            if ( connection.DestConnector == null )
            {
                connection.DestConnectorHotspot = curDragPoint;
            }
            else
            {
                connection.SourceConnectorHotspot = curDragPoint;
            }
        }

        /// <summary>
        /// Checks if connection that is currently being dragged is valid.      
        /// </summary>
        public async Task<bool> QueryConnectionFeedbackAsync( ConnectorViewModel draggedOutConnector, ConnectorViewModel draggedOverConnector )
        {
            if ( draggedOutConnector == draggedOverConnector )
            {
                //
                // Can't connect to self!
                // Provide feedback to indicate that this connection is not valid!
                //
                return false;
            }

            var sourceConnector = draggedOutConnector;
            var destConnector = draggedOverConnector;

            //
            // Only allow connections from output connector to input connector (ie each
            // connector must have a different type).
            // Also only allocation from one node to another, never one node back to the same node.
            //
            return await IsConnectionValidAsync( sourceConnector, destConnector );
        }

        /// <summary>
        /// Retrieve a connection between the two connectors.
        /// Returns null if there is no connection between the connectors.
        /// </summary>
        public ConnectionViewModel FindConnection( ConnectorViewModel connector1, ConnectorViewModel connector2 )
        {
            Trace.Assert( connector1.Type != connector2.Type );

            //
            // Figure out which one is the source connector and which one is the
            // destination connector based on their connector types.
            //
            var sourceConnector = connector1.Type == ConnectorType.Output ? connector1 : connector2;
            var destConnector = connector1.Type == ConnectorType.Output ? connector2 : connector1;

            //
            // Now we can just iterate attached connections of the source
            // and see if it each one is attached to the destination connector.
            //

            foreach ( var connection in sourceConnector.AttachedConnections )
            {
                if ( connection.DestConnector == destConnector )
                {
                    //
                    // Found a connection that is outgoing from the source connector
                    // and incoming to the destination connector.
                    //
                    return connection;
                }
            }

            return null;
        }

        /// <summary>
        /// Add node.
        /// </summary>
        /// <param name="node">Node that we want to add.</param>
        public void AddNode( NodeViewModel node )
        {
            node.DiagramViewModel = this;
            Network.Nodes.Add( node );
        }

        /// <summary>
        /// Add connection and set current orientation
        /// </summary>
        /// <param name="connectionViewModel"></param>
        public void AddConnection( ConnectionViewModel connectionViewModel )
        {
            connectionViewModel.Orientation = Orientation;
            connectionViewModel.DiagramViewModel = this;
            Network.Connections.Add( connectionViewModel );
        }

        // quick fix
		public void DeleteInvalidConnections()
        {
            Network.Connections.Where( connection => ( connection.DestConnector == null || connection.SourceConnector == null ) ).ToList().ForEach( connection => DeleteConnection( connection ) );   
        }

        /// <summary>
        /// Delete node.
        /// </summary>
        /// <param name="node">Node that we want to delete.</param>
        public void DeleteNode( NodeViewModel node )
        {
            Network.Connections.RemoveRange( node.AttachedConnections );
            Network.Nodes.Remove( node );
        }

        /// <summary>
        /// Find node.
        /// </summary>
        /// <returns>Node of given ID or null if there is none.</returns>
        public NodeViewModel FindNode( string nodeID )
        {
            return Network.Nodes.FirstOrDefault( item => item.ID == nodeID );
        }

        /// <summary>
        /// Delete connection.
        /// </summary>
        /// <param name="connection">Connection that we want to delete.</param>
        public void DeleteConnection( ConnectionViewModel connection )
        {
            Network.Connections.Remove( connection );
        }

        /// <summary>
        /// Creates new diagram by setting up network view model.
        /// </summary>
        /// <param name="nodes">Diagram nodes.</param>
        /// <param name="connections">Diagram connections.</param>
        public void CreateDiagram( List<NodeViewModel> nodes, List<ConnectionViewModel> connections )
        {
            SetupNetworkViewModel( nodes, connections );
        }

        #endregion Public methods

        #region Private methods

        /// <summary>
        /// Creates new network model and adds nodes and connections that will be displayed in node control.
        /// </summary>
        /// <param name="nodes">List of nodes that will be displayed in node control.</param>
        /// <param name="connections">List of connections that will be displayed in node control.</param>
        private void SetupNetworkViewModel( IEnumerable<NodeViewModel> nodes, ICollection<ConnectionViewModel> connections )
        {
            Network = new NetworkViewModel();
            AddNodes( nodes );
            AddConnections( connections );
        }

        /// <summary>
        /// Add nodes to current diagram.
        /// </summary>
        /// <param name="nodes"></param>
        private void AddNodes( IEnumerable<NodeViewModel> nodes )
        {
            foreach ( var node in nodes )
                AddNode( node );
        }

        /// <summary>
        /// Add connections and set current orientation for each of them.
        /// </summary>
        /// <param name="connections"></param>
        private void AddConnections( ICollection<ConnectionViewModel> connections )
        {
            foreach ( var connection in connections )
            {
                connection.Orientation = Orientation;
                connection.DiagramViewModel = this;
            }

            Network.Connections.AddRange( connections );
        }

        /// <summary>
        /// Checks if connection drawn between two connectors is valid.
        /// Connection is not valid if drawn onto itself.
        /// Connection is not valid if connector types are the same (for example: drawing connection from input to input or from output to output).
        /// Connection is not valid if types of properties specified by Binding property are not compatible.
        /// </summary>
        private async Task<bool> IsConnectionValidAsync( ConnectorViewModel sourceConnector, ConnectorViewModel destConnector )
        {
            // During creating connection one of connectors can be null
            if ( destConnector == null || sourceConnector == null )
                return true;

            return sourceConnector.ParentNode != destConnector.ParentNode &&
                   sourceConnector.Type != destConnector.Type &&
                   await AreConnectorPropertiesValidAsync( sourceConnector, destConnector );
        }

        /// <summary>
        /// Method to check type compatibility of properties bound to source and destination connectors.
        /// </summary>
        /// <param name="connectorDraggedOut">Source connector.</param>
        /// <param name="connectorDraggedOver">Destination connector</param>
        /// <returns>Are types compatible.</returns>
        private async Task<bool> AreConnectorPropertiesValidAsync( ConnectorViewModel connectorDraggedOut, ConnectorViewModel connectorDraggedOver )
        {
            var inputConnector = connectorDraggedOut.Type == ConnectorType.Input
                ? connectorDraggedOut
                : connectorDraggedOver;

            var outputConnector = connectorDraggedOut.Type == ConnectorType.Output
                ? connectorDraggedOut
                : connectorDraggedOver;

            return await inputConnector.Property.IsCompatibleWithAsync( outputConnector.Property );
        }

        #endregion Private methods
    }
}
